#ifndef __MEDIAMANAGER_HPP__
#define __MEDIAMANAGER_HPP__ 1

#include <cstdint>
#include <string>
#include <iostream>
#include <fstream>
#include <list>
#include <type_traits>

using namespace std;

enum class MediaSource : int32_t
{
	MEDIA_SOURCE_DISK =					0,
	MEDIA_SOURCE_ARCHIVE =				1,
};

struct DiskMedia { };
struct ArchiveMedia { };

struct MediaCommon
{
	MediaSource source;
	ios_base::openmode accessMode;
	fstream stream;
	string fileName;
};

template <typename T, typename enabled = void>
struct Media { };

template<typename T>
struct Media<T, typename enable_if<is_same<T, DiskMedia>::value>::type> : MediaCommon 
{
};

template<typename T>
struct Media<T, typename enable_if<is_same<T, ArchiveMedia>::value>::type> : MediaCommon 
{
	string archiveName;
};

class CMediaManager
{
public:
	typedef list<Media<DiskMedia>*> DiskMediaTable;
	typedef list<Media<ArchiveMedia>*> ArchiveMediaTable;

	CMediaManager()
	{
		;;
	}

	~CMediaManager()
	{

		if(diskMediaTable.size())
		{
			auto begin = diskMediaTable.begin();
			auto end = diskMediaTable.end();
			for(; begin != end;)
			{
				if((*begin)->stream.is_open())
					(*begin)->stream.close();
				delete (*begin);
				begin = diskMediaTable.erase(begin);
			}
		}

		if(archiveMediaTable.size())
		{
			auto begin = archiveMediaTable.begin();
			auto end = archiveMediaTable.end();
			for(; begin != end;)
			{
				if((*begin)->stream.is_open())
					(*begin)->stream.close();
				delete (*begin);
				begin = archiveMediaTable.erase(begin);
			}
		}
	}

	template <typename T> streamoff GetSize(Media<T>* media)
	{
		if(!media)
			return 0;

		media->stream.seekg(0, ios::end);
		return media->stream.tellg();
	}

	template <typename T> Media<T>* Open(ios::openmode accessMode, string fileName);

	template <> Media<DiskMedia>* Open<DiskMedia>(ios::openmode accessMode, string fileName)
	{
		Media<DiskMedia>* media = new Media<DiskMedia>;

		media->source = MediaSource::MEDIA_SOURCE_DISK;
		media->accessMode = accessMode;
		media->fileName = fileName;
		media->stream = fstream();
		//media->stream.exceptions(fstream::failbit|fstream::badbit);
		try
		{
			media->stream.open(fileName.c_str(), accessMode);
			if(!media->stream.good())
				throw exception("could not open");
		}
		catch(exception& e)
		{
			cout << "Caught: `" << e.what() << "`" << endl;
			delete media;
			return 0;
		}

		diskMediaTable.push_back(media);

		return media;		
	}

	template <typename T> bool Close(Media<T>* media)
	{
		if(media == nullptr)
			return false;

		if(media->stream && media->stream.is_open())
			media->stream.close();

		if(media->source == MEDIA_SOURCE_DISK && diskList.size())
		{
			auto begin = diskList.begin();
			auto end = diskList.end();
			for(; begin != end;)
			{
				if(*begin == media)
				{
					delete (*begin);
					diskList.erase(begin);
					return true;
				}
			}
		}

		if(media->source == MEDIA_SOURCE_ARCHIVE && archiveList.size())
		{
			auto begin = archiveList.begin();
			auto end = archiveList.end();
			for(; begin != end;)
			{
				if(*begin == media)
				{
					delete (*begin);
					archiveList.erase(begin);
					return true;
				}
			}
		}

		return false;
	}

	template <typename T> streamoff Read(Media<T>* media, char* buffer, streamoff offset, size_t numBytes)
	{
		if(numBytes == 0)
			return 0;

		if(!media)
			return 0;

		if(!media->stream || media->stream.fail())
			return 0;

		streamoff length = GetSize(media);
		if(offset && offset > length)
			return 0;

		if(numBytes > length || offset + numBytes > length)
			return 0;

		streamoff read = 0;

		media->stream.seekg(offset, ios::beg);
		media->stream.read(buffer, numBytes);
		read = media->stream.gcount();
		if(media->stream.fail())
			return 0;

		return read;
	}

	template <typename T> streamsize Read(Media<T>* media, char* outbuf)
	{
		streamsize length = GetSize(media);
		outbuf = new char[length];
		//char* buffer = new char[length];
#if 0
		if(media->stream.binary)
		{
			while(!stream.eof())
			{
				gstream.getline();
			}
		}
		else
		{
			streamsize read = Read(media, &outbuf[0], ios::beg, length);
		}
#endif
		streamsize read = Read(media, &outbuf[0], ios::beg, length);
		//outbuf = buffer;
		return read;
	}

	template <typename T> bool Write(Media<T>* media, char* buffer, streamoff offset, streamsize numBytes)
	{
		if(numBytes == 0)
			return false;

		if(!media)
			return false;

		if(!media->stream || media->stream.fail())
			return false;

		media->stream.seekp(offset);
		media->stream.write(buffer, numBytes);

		if(media->stream.fail())
			return false;

		return true;
	}

private:
	DiskMediaTable diskMediaTable;
	ArchiveMediaTable archiveMediaTable;
};

#endif
